<?php


namespace BeReborn\Pool;


use BeReborn\Base\Component;
use BeReborn\Http\Context;
use BeReborn\Base\Config;
use BeReborn\Exception\ComponentException;
use BeReborn\Exception\ConfigException;
use Exception;
use Swoole\Coroutine;
use Swoole\Coroutine\Channel;
use Swoole\Timer;

/**
 * Class Pool
 * @package BeReborn\Pool
 */
abstract class Pool extends Component
{

    /** @var Channel[] */
    private $_items = [];

    public $max = 60;

    public $creates = -1;

    public $lastTime = 0;


    /**
     * @return array
     * @throws ConfigException
     */
    private function getClearTime(): array
    {
        $firstClear = Config::get('pool.clear.start', false, 60);
        $lastClear = Config::get('pool.clear.end', false, 30);
        return [$firstClear, $lastClear];
    }


    /**
     * @throws Exception
     */
    public function Heartbeat_detection()
    {
        if (env('state') == 'exit') {
            $this->flush(0);
            return;
        }
        if ($this->lastTime == 0) {
            return;
        }
        [$firstClear, $lastClear] = $this->getClearTime();
        if ($this->lastTime + $firstClear < time()) {
            $this->flush(0);
        } else if ($this->lastTime + $lastClear < time()) {
            $this->flush(2);
        }
    }


    /**
     * @param $retain_number
     * @throws Exception
     */
    protected function flush($retain_number)
    {
        $channels = $this->getChannels();
        foreach ($channels as $name => $channel) {
            $names[] = $name;
            $this->pop($channel, $name, $retain_number);
        }
        if ($retain_number == 0) {
            $this->debug('release Timer::tick');
            Timer::clear($this->creates);
            $this->creates = -1;
        }
    }


    /**
     * @param $channel
     * @param $name
     * @param $retain_number
     * @throws Exception
     */
    protected function pop($channel, $name, $retain_number)
    {
        while ($channel->length() > $retain_number) {
            $connection = $channel->pop();
            if ($connection) {
                unset($connection);
            }
            $this->desc($name);
        }
    }


    /**
     * @param $driver
     * @param $name
     * @param false $isMaster
     * @param int $max
     */
    public function initConnections($driver, $name, bool $isMaster = false, $max = 60)
    {
        $name = $this->name($driver, $name, $isMaster);
        if (isset($this->_items[$name]) && $this->_items[$name] instanceof Channel) {
            return;
        }
        if (!Context::inCoroutine()) {
            return;
        }
        $this->_items[$name] = new Channel((int)$max);
        $this->max = (int)$max;
    }


    /**
     * @param $name
     * @param array $callback
     * @return array
     * @throws Exception
     */
    protected function getFromChannel($name, $callback)
    {
        if (!Context::inCoroutine()) {
            return $this->createClient($name, $callback);
        }
        if (!$this->hasItem($name)) {
	        return $this->createClient($name, $callback);
        }
        $connection = $this->_items[$name]->pop(-1);
        if (!$this->checkCanUse($name, $connection)) {
            return $this->createClient($name, $callback);
        } else {
            return $connection;
        }
    }


    /**
     * @param $name
     * @param $callback
     * @throws Exception
     */
    private function createByCallback($name, $callback)
    {
        if ($this->creates === -1 && !is_callable($callback)) {
            $this->creates = Timer::tick(1000, [$this, 'Heartbeat_detection']);
        }
        if (!Context::hasContext('create::client::ing::' . $name)) {
            $this->push($name, $this->createClient($name, $callback));
            Context::remove('create::client::ing::' . $name);
        }
    }


    /**
     * @param $cds
     * @param $coroutineName
     * @param false $isBefore
     * @throws ComponentException
     */
    public function printClients($cds, $coroutineName, $isBefore = false)
    {
        #$this->warning(($isBefore ? 'before ' : '') . 'create client[address: ' . $cds . ', ' . env('worker', 0) . ', coroutine: ' . Coroutine::getCid() . ', has num: ' . $this->size($coroutineName) . ']');
    }


    abstract public function createClient(string $name, $config);


    /**
     * @param $driver
     * @param $cds
     * @param false $isMaster
     * @return string
     */
    public function name($driver, $cds, $isMaster = false)
    {
        if ($isMaster === true) {
            return $cds . '_master';
        } else {
            return $cds . '_slave';
        }
    }


    /**
     * @param string $name
     * @param $client
     * @return mixed
     * 检查连接可靠性
     */
    public function checkCanUse(string $name, $client)
    {
        return true;
    }

    /**
     * @param $name
     * @throws Exception
     */
    public function desc(string $name)
    {
        throw new Exception('Undefined system processing function.');
    }


    /**
     * @param array $config
     * @param $isMaster
     * @return mixed
     * @throws Exception
     */
    public function get($config, bool $isMaster)
    {
        throw new Exception('Undefined system processing function.');
    }


    /**
     * @param $name
     * @return bool
     */
    public function hasItem(string $name): bool
    {
        if (!isset($this->_items[$name])) {
	        $this->_items[$name] = new Channel($this->max);
        }
	    return !$this->_items[$name]->isEmpty();
    }


    /**
     * @param $name
     * @return mixed
     */
    public function size(string $name)
    {
        if (!Context::inCoroutine()) {
            return 0;
        }
        if (!isset($this->_items[$name])) {
            return 0;
        }
        return $this->_items[$name]->length();
    }


    /**
     * @param $name
     * @param $client
     */
    public function push(string $name, $client)
    {
        if (!Context::inCoroutine()) {
            return;
        }
        if (!isset($this->_items[$name])) {
            $this->_items[$name] = new Channel($this->max);
        }
        if (!$this->_items[$name]->isFull()) {
            $this->_items[$name]->push($client);
        }
    }


    /**
     * @param string $name
     * @throws Exception
     */
    public function clean(string $name)
    {
        if (!Context::inCoroutine() || !isset($this->_items[$name])) {
            return;
        }
        $channel = $this->_items[$name];
        $this->pop($channel, $name, 0);
        if ($this->creates > -1) {
            Timer::clear($this->creates);
        }
        $this->_items[$name] = null;
    }


    /**
     * @return Channel[]
     */
    protected function getChannels(): array
    {
        return $this->_items;
    }
}
